#include "ukey/ukey.h"

#include "ukey/ukey.h"
#include <cryptoki_linux.h>
#include <cryptoki_ext.h>
#include <auxiliary.h>
#include <string.h>
#include <gwbase/base/string_util.h>
#include <gwbase/base/logging.h>
#include <gwbase/base/mutex.h>

using namespace dicom;

namespace ukey {

#define _countof(a) (sizeof(a) / sizeof(a[0]))

char SURDOC_APP_NAME[] = "SurDoc#App";
char SURDOC_TOKEN_NAME[] = "ePass2003_DRA";
char SURDOC_ACCOUNT_LABEL[] = "SurDoc#Account";
char SURDOC_PRIV_KEY_CIPHER_LABEL[] = "SurDoc#PriKeyCipher";
char SURDOC_RSA_PRIV_KEY_LABEL[] = "SurDoc#RsaPrivKey";
char SURDOC_RSA_PUB_KEY_LABEL[] = "SurDoc#RsaPubKey";

static CK_BBOOL kCKTrue = CK_TRUE;
static CK_BBOOL kCKFalse = CK_FALSE;
static CK_OBJECT_CLASS kCKDataClass = CKO_DATA;
static CK_OBJECT_CLASS kCKPrivKeyClass = CKO_PRIVATE_KEY;
static CK_OBJECT_CLASS kCKPubKeyClass = CKO_PUBLIC_KEY;
static CK_ULONG kRsaKeyType = CKK_RSA;
static CK_ULONG kModulusBitLen = 2048;

static const CK_ATTRIBUTE kAccountFindTemplate[] = {
    {CKA_CLASS, &kCKDataClass, sizeof(kCKDataClass)},
    {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
    {CKA_LABEL, SURDOC_ACCOUNT_LABEL, strlen(SURDOC_ACCOUNT_LABEL)}, };

static const CK_ATTRIBUTE kPrivKeyCipherFindTemplate[] = {
    {CKA_CLASS, &kCKDataClass, sizeof(kCKDataClass)},
    {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
    {CKA_LABEL, SURDOC_PRIV_KEY_CIPHER_LABEL,
     strlen(SURDOC_PRIV_KEY_CIPHER_LABEL)}, };

static const CK_ATTRIBUTE kPrivKeyFindTemplate[] =
    {{CKA_CLASS, &kCKPrivKeyClass, sizeof(kCKPrivKeyClass)},
     {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
     {CKA_SUBJECT, SURDOC_RSA_PRIV_KEY_LABEL,
      strlen(SURDOC_RSA_PRIV_KEY_LABEL)}, };

static const CK_ATTRIBUTE kPubKeyFindTemplate
    [] = {{CKA_CLASS, &kCKPubKeyClass, sizeof(kCKPubKeyClass)},
          {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
          {CKA_SUBJECT, SURDOC_RSA_PUB_KEY_LABEL,
           strlen(SURDOC_RSA_PUB_KEY_LABEL)}, };

static const CK_MECHANISM kKeyGenMechanism = {CKM_RSA_PKCS_KEY_PAIR_GEN,
                                              NULL_PTR, 0};

static const CK_ATTRIBUTE kPrivKeyCreateTemplate[] = {
    {CKA_CLASS, &kCKPrivKeyClass, sizeof(kCKPrivKeyClass)},
    {CKA_KEY_TYPE, &kRsaKeyType, sizeof(kRsaKeyType)},
    {CKA_SUBJECT, SURDOC_RSA_PRIV_KEY_LABEL, strlen(SURDOC_RSA_PRIV_KEY_LABEL)},
    {CKA_DECRYPT, &kCKTrue, sizeof(kCKTrue)},
    {CKA_PRIVATE, &kCKTrue, sizeof(kCKTrue)},
    {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
    {CKA_SENSITIVE, &kCKTrue, sizeof(kCKTrue)},
    {CKA_EXTRACTABLE, &kCKFalse, sizeof(kCKFalse)},
    {CKA_UNWRAP, &kCKTrue, sizeof(kCKTrue)}, };

static const CK_ATTRIBUTE kPubKeyCreateTemplate[] = {
    {CKA_CLASS, &kCKPubKeyClass, sizeof(kCKPubKeyClass)},
    {CKA_KEY_TYPE, &kRsaKeyType, sizeof(kRsaKeyType)},
    {CKA_SUBJECT, SURDOC_RSA_PUB_KEY_LABEL, strlen(SURDOC_RSA_PUB_KEY_LABEL)},
    {CKA_MODULUS_BITS, &kModulusBitLen, sizeof(kModulusBitLen)},
    {CKA_ENCRYPT, &kCKTrue, sizeof(kCKTrue)},
    {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
    {CKA_WRAP, &kCKTrue, sizeof(kCKTrue)}, };

static const CK_MECHANISM kEncMechanism = {CKM_RSA_PKCS, NULL_PTR, 0};

static Status DestroyObjectsByTemplate(CK_SESSION_HANDLE session,
                                       const CK_ATTRIBUTE *templ, size_t count,
                                       int *del_count);

static Status GenerateRsaKeyPair(CK_SESSION_HANDLE session,
                                 CK_OBJECT_HANDLE *prikey,
                                 CK_OBJECT_HANDLE *pubkey);

static Status OpenUserSession(CK_SLOT_ID slot_id, const std::string &pin,
                              bool readonly, CK_SESSION_HANDLE *session);

static Status OpenSOSession(CK_SLOT_ID slot_id, const std::string &sopin,
                            CK_SESSION_HANDLE *session);

static Status SaveDataObject(CK_SESSION_HANDLE session,
                             const std::string &label, void *data, size_t len,
                             CK_OBJECT_HANDLE *obj);

static Status LoadDataObject(CK_SESSION_HANDLE session,
                             const std::string &label, ByteArray *out);

static Status FindObjectsByTemplate(CK_SESSION_HANDLE session,
                                    const CK_ATTRIBUTE *templ, size_t count,
                                    std::vector<CK_OBJECT_HANDLE> *out);

static CK_UTF8CHAR_PTR ck_utf8char_ptr(const char *str) {
  char *tmp = const_cast<char *>(str);
  return reinterpret_cast<CK_UTF8CHAR_PTR>(tmp);
}

static void *void_ptr(const void *str) {
  void *tmp = const_cast<void *>(str);
  return static_cast<void *>(tmp);
}

UKeyHelper::UKeyHelper(void) {}
UKeyHelper::~UKeyHelper(void) {}

static Status HandleEPassError(CK_RV rv, const char *func) {
  std::string msg;
  dicom::StringPrintf(&msg, "PKCS#11::%s() failed: %08lx", func, rv);
  return Status(Status::kOther, msg);
}

class PKCS_SessionHandle {
 public:
  PKCS_SessionHandle(CK_SESSION_HANDLE s) : s_(s) {}
  ~PKCS_SessionHandle() { ::C_CloseSession(s_); }

 private:
  CK_SESSION_HANDLE s_;
};

Status UKeyHelper::GetTokenList(std::vector<TokenInfo> *list) {
  Status s = GlobalInit();
  if (!s.ok()) return s;

  CK_RV rv;
  CK_ULONG slot_num;
  rv = ::C_GetSlotList(TRUE, NULL_PTR, &slot_num);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_GetSlotList");
  }

  if (slot_num == 0) return Status();

  CK_SLOT_ID_PTR slots = new CK_SLOT_ID[slot_num];
  if (slots == NULL_PTR) {
    LOG_ERROR << "OOM: failed to allocate CK_SLOT_ID list";
    return Status(Status::kOther, "OOM: failed to allocate CK_SLOT_ID list");
  }

  rv = ::C_GetSlotList(TRUE, slots, &slot_num);
  if (rv != CKR_OK) {
    delete[] slots;
    return HandleEPassError(rv, "C_GetSlotList");
  }

  if (slot_num == 0) {
    delete[] slots;
    return Status();
  }

  for (CK_ULONG i = 0; i < slot_num; ++i) {
    CK_TOKEN_INFO info;
    ::C_GetTokenInfo(slots[i], &info);
    if (rv != CKR_OK) {
      delete[] slots;
      return HandleEPassError(rv, "C_GetTokenInfo");
    }

    TokenInfo ti;
    ti.slot_id = static_cast<uint32_t>(slots[i]);
    ti.label.assign(info.label, info.label + 32);
    ti.serial_number.assign(info.serialNumber, info.serialNumber + 16);
    list->push_back(ti);
  }

  delete[] slots;
  return Status();
}

Status UKeyHelper::FormatToken(uint32_t slot_id, const std::string &token_name,
                               const std::string &sopin,
                               const std::string &pin) {
  CK_RV rv;
  Status s = GlobalInit();
  if (!s.ok()) return s;

  AUX_INIT_TOKEN_LOWLEVL_PKI param;
  bzero(&param, sizeof(param));
  param.version.major = 0x01;
  param.version.minor = 0x00;
  param.strTokenName = const_cast<char *>(token_name.c_str());
  param.strSOPin = const_cast<char *>(sopin.c_str());
  param.strUserPin = const_cast<char *>(pin.c_str());
  param.ucSOMaxPinEC = 6;
  param.ucUserMaxPinEC = 6;
  param.nRSAKeyPairCount = 9;
  param.ulPubSize = 15000;
  param.ulPrvSize = 10000;
  param.nDSAKeyPairCount = 9;

  AUX_FUNC_LIST_PTR pAuxFunc = NULL_PTR;
  rv = ::E_GetAuxFunctionList(&pAuxFunc);
  if (CKR_OK != rv || NULL_PTR == pAuxFunc) {
    return HandleEPassError(rv, "E_GetAuxFunctionList");
  }

  EP_InitTokenPrivate intiTokenPrivateFunc =
      reinterpret_cast<EP_InitTokenPrivate>(
          pAuxFunc->pFunc[EP_INIT_TOKEN_PRIVATE]);
  rv = intiTokenPrivateFunc(slot_id, &param);
  if (CKR_OK != rv) {
    return HandleEPassError(rv, "EP_InitTokenPrivate");
  }

  return Status();
}

Status OpenUserSession(CK_SLOT_ID slot_id, const std::string &pin,
                       bool readonly, CK_SESSION_HANDLE *session) {
  CK_RV rv;

  Status s = UKeyHelper::GlobalInit();
  if (!s.ok()) return s;

  CK_FLAGS flags = CKF_SERIAL_SESSION;
  if (!readonly) flags |= CKF_RW_SESSION;

  rv = ::C_OpenSession(slot_id, flags, SURDOC_APP_NAME, NULL_PTR, session);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_OpenSession");
  }

  rv = ::C_Login(*session, CKU_USER, ck_utf8char_ptr(pin.c_str()), pin.size());

  if (rv != CKR_OK) {
    ::C_CloseSession(*session);
    *session = NULL_PTR;
    s = HandleEPassError(rv, "C_Login");
    if (rv == CKR_PIN_INCORRECT) {
      LOG_ERROR << "login token with incorrect PIN";
      return s;
    } else if (rv == CKR_PIN_LOCKED) {
      LOG_ERROR << "ePass token PIN locked";
      return s;
    }
    return s;
  }

  return Status();
}

static Status OpenSOSession(CK_SLOT_ID slot_id, const std::string &sopin,
                            CK_SESSION_HANDLE *session) {
  CK_RV rv;

  Status s = UKeyHelper::GlobalInit();
  if (!s.ok()) return s;

  rv = ::C_OpenSession(slot_id, CKF_RW_SESSION | CKF_SERIAL_SESSION,
                       SURDOC_APP_NAME, NULL_PTR, session);

  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_OpenSession");
  }

  rv =
      ::C_Login(*session, CKU_SO, ck_utf8char_ptr(sopin.c_str()), sopin.size());
  if (rv != CKR_OK) {
    ::C_CloseSession(*session);
    *session = NULL_PTR;
    s = HandleEPassError(rv, "C_Login");
    if (rv == CKR_PIN_INCORRECT) {
      LOG_ERROR << "login token with incorrect SOPIN";
      return s;
    }
    return s;
  }

  return Status();
}

Status UKeyHelper::CheckPin(uint32_t slot_id, const std::string &pin) {
  CK_SESSION_HANDLE session = NULL_PTR;

  Status s = GlobalInit();
  if (!s.ok()) return s;

  s = OpenUserSession(slot_id, pin, true, &session);
  if (!s.ok()) return s;

  ::C_CloseSession(session);
  return Status();
}

Status UKeyHelper::UnlockPin(uint32_t slot_id, const std::string &sopin,
                             const std::string &new_pin) {
  CK_RV rv;
  CK_SESSION_HANDLE session = NULL_PTR;

  Status s = GlobalInit();
  if (!s.ok()) return s;

  s = OpenSOSession(slot_id, sopin, &session);
  if (!s.ok()) return s;

  PKCS_SessionHandle h(session);

  // C_InitPINcan only be called in the “R/W SO Functions” state
  rv = ::C_InitPIN(session, ck_utf8char_ptr(new_pin.c_str()), new_pin.size());
  if (CKR_OK != rv) {
    return HandleEPassError(rv, "C_InitPIN");
  }

  return Status();
}

Status UKeyHelper::SetTokenPin(uint32_t slot_id, const std::string &oldpin,
                               const std::string &pin) {
  CK_RV rv;
  CK_SESSION_HANDLE session = NULL_PTR;

  Status s = GlobalInit();
  if (!s.ok()) return s;

  rv = ::C_OpenSession(slot_id, CKF_RW_SESSION | CKF_SERIAL_SESSION,
                       SURDOC_APP_NAME, NULL_PTR, &session);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_OpenSession");
  }

  PKCS_SessionHandle h(session);
  rv = ::C_SetPIN(session, ck_utf8char_ptr(oldpin.c_str()), oldpin.size(),
                  ck_utf8char_ptr(pin.c_str()), pin.size());

  if (rv != CKR_OK) {
    s = HandleEPassError(rv, "C_Login");
    if (rv == CKR_PIN_INCORRECT) {
      LOG_ERROR << "login ePass token with incorrect pin";
      return s;
    } else if (rv == CKR_PIN_INVALID || rv == CKR_PIN_LEN_RANGE) {
      LOG_ERROR << "new pin is invalid";
      return s;
    }
    return s;
  }

  return Status();
}

static Status DestroyObjectsByTemplate(CK_SESSION_HANDLE session,
                                       const CK_ATTRIBUTE *templ, size_t count,
                                       int *del_count) {
  CK_RV rv;
  *del_count = 0;
  rv = ::C_FindObjectsInit(session, const_cast<CK_ATTRIBUTE *>(templ), count);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_FindObjectsInit");
  }

  while (true) {
    CK_OBJECT_HANDLE obj;
    CK_ULONG obj_count;
    rv = ::C_FindObjects(session, &obj, 1, &obj_count);
    if (rv != CKR_OK) {
      ::C_FindObjectsFinal(session);
      return HandleEPassError(rv, "C_FindObjectsInit");
    }

    if (obj_count == 0) break;

    assert(obj_count == 1);
    rv = ::C_DestroyObject(session, obj);
    if (rv != CKR_OK) {
      ::C_FindObjectsFinal(session);
      return HandleEPassError(rv, "C_DestroyObject");
    }
    ++(*del_count);
  }

  ::C_FindObjectsFinal(session);
  return Status();
}

Status UKeyHelper::ClearSurDocCredential(uint32_t slot_id,
                                         const std::string &pin) {
  CK_SESSION_HANDLE session = NULL_PTR;

  Status s = GlobalInit();
  if (!s.ok()) return s;

  s = OpenUserSession(slot_id, pin, false, &session);
  if (!s.ok()) return s;

  PKCS_SessionHandle h(session);

  int count;
  s = DestroyObjectsByTemplate(session, kAccountFindTemplate,
                               _countof(kAccountFindTemplate), &count);
  if (!s.ok()) {
    LOG_ERROR << "failed to delete account object: " << s.ToString().c_str();
    return s;
  } else {
    LOG_DEBUG << "delete account object count: " << count;
  }

  s = DestroyObjectsByTemplate(session, kPrivKeyCipherFindTemplate,
                               _countof(kPrivKeyCipherFindTemplate), &count);
  if (!s.ok()) {
    LOG_ERROR << "failed to delete priv key cipher object: " << s;
    return s;
  } else {
    LOG_DEBUG << "delete priv key cipher object count: " << count;
  }

  s = DestroyObjectsByTemplate(session, kPrivKeyFindTemplate,
                               _countof(kPrivKeyFindTemplate), &count);
  if (!s.ok()) {
    LOG_ERROR << "failed to delete RSA priv key object: " << s;
    return s;
  } else {
    LOG_DEBUG << "delete RSA priv key object count: " << count;
  }

  s = DestroyObjectsByTemplate(session, kPubKeyFindTemplate,
                               _countof(kPubKeyFindTemplate), &count);
  if (!s.ok()) {
    LOG_ERROR << "failed to delete RSA pub key object: " << s;
    return s;
  } else {
    LOG_DEBUG << "delete RSA pub key object count: " << count;
  }

  return Status();
}

static Status LoadDataObject(CK_SESSION_HANDLE session,
                             const std::string &label, ByteArray *out) {
  CK_RV rv;
  Status s;

  std::vector<CK_OBJECT_HANDLE> objs;
  CK_ATTRIBUTE find_template[] = {
      {CKA_CLASS, &kCKDataClass, sizeof(kCKDataClass)},
      {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
      {CKA_LABEL, void_ptr(label.c_str()), label.size()}, };

  s = FindObjectsByTemplate(session, find_template, _countof(find_template),
                            &objs);
  if (!s.ok()) return s;

  if (objs.empty()) {
    LOG_ERROR << "can't find data object with label: " << label;
    return Status(Status::kOther, "can't find data object with this label");
  }

  CK_ULONG val_len = 0;
  CK_ATTRIBUTE get_template[] = {{CKA_VALUE, NULL_PTR, 0}, };

  rv = ::C_GetAttributeValue(session, objs[0], get_template,
                             _countof(get_template));
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_GetAttributeValue");
  }

  val_len = get_template[0].ulValueLen;
  CK_BYTE *val = new CK_BYTE[val_len];
  memset(val, 0, val_len);
  get_template[0].pValue = val;

  rv = ::C_GetAttributeValue(session, objs[0], get_template,
                             _countof(get_template));
  if (rv != CKR_OK) {
    delete[] val;
    return HandleEPassError(rv, "C_GetAttributeValue");
  }

  out->assign(val, val + val_len);
  delete[] val;

  return Status();
}

static Status FindObjectsByTemplate(CK_SESSION_HANDLE session,
                                    const CK_ATTRIBUTE *templ, size_t count,
                                    std::vector<CK_OBJECT_HANDLE> *out) {
  CK_RV rv;
  rv = ::C_FindObjectsInit(session, const_cast<CK_ATTRIBUTE *>(templ), count);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_FindObjectsInit");
  }

  while (true) {
    CK_OBJECT_HANDLE obj;
    CK_ULONG obj_count;
    rv = ::C_FindObjects(session, &obj, 1, &obj_count);
    if (rv != CKR_OK) {
      ::C_FindObjectsFinal(session);
      return HandleEPassError(rv, "C_FindObjectsInit");
    }

    if (obj_count == 0) {
      ::C_FindObjectsFinal(session);
      if (out->empty()) {
        return Status(Status::kOther, "FindObjectsByTemplate() find nothing");
      } else {
        return Status();
      }
    }

    assert(obj_count == 1);
    out->push_back(obj);
  }

  ::C_FindObjectsFinal(session);
  return Status();
}

Status UKeyHelper::LoadSurDocCredential(uint32_t slot_id,
                                        const std::string &pin,
                                        std::string *account,
                                        ByteArray *credential) {
  ByteArray account_b;
  ByteArray credential_b;
  Status s;
  CK_RV rv;
  CK_SESSION_HANDLE session = NULL_PTR;
  std::vector<CK_OBJECT_HANDLE> priv_key_objs;
  CK_OBJECT_HANDLE privkey;

  s = GlobalInit();
  if (!s.ok()) return s;

  s = OpenUserSession(slot_id, pin, true, &session);
  if (!s.ok()) {
    return s;
  }

  PKCS_SessionHandle h(session);

  s = LoadDataObject(session, SURDOC_ACCOUNT_LABEL, &account_b);
  if (!s.ok()) {
    return s;
  }

  s = LoadDataObject(session, SURDOC_PRIV_KEY_CIPHER_LABEL, &credential_b);
  if (!s.ok()) {
    return s;
  }

  s = FindObjectsByTemplate(session, kPrivKeyFindTemplate,
                            _countof(kPrivKeyFindTemplate), &priv_key_objs);
  if (!s.ok()) {
    return s;
  }

  if (priv_key_objs.empty()) {
    LOG_ERROR << "can't find RSA priv key";
    return Status(Status::kOther, "can't find RSA priv key");
  }

  privkey = priv_key_objs[0];
  rv = ::C_DecryptInit(session, const_cast<CK_MECHANISM_PTR>(&kEncMechanism),
                       privkey);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_DecryptInit");
  }

  CK_ULONG plainlen = 0;
  rv = ::C_Decrypt(session, &credential_b[0], credential_b.size(), NULL_PTR,
                   &plainlen);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_Decrypt");
  }

  CK_BYTE *plain = new CK_BYTE[plainlen];
  rv = ::C_Decrypt(session, &credential_b[0], credential_b.size(), plain,
                   &plainlen);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_Decrypt");
  }

  *account = account_b.toRawStr();
  credential->assign(plain, plain + plainlen);
  return Status();
}

static Status GenerateRsaKeyPair(CK_SESSION_HANDLE session,
                                 CK_OBJECT_HANDLE *prikey,
                                 CK_OBJECT_HANDLE *pubkey) {
  CK_RV rv;
  rv = ::C_GenerateKeyPair(session,
                           const_cast<CK_MECHANISM_PTR>(&kKeyGenMechanism),
                           const_cast<CK_ATTRIBUTE_PTR>(kPubKeyCreateTemplate),
                           _countof(kPubKeyCreateTemplate),
                           const_cast<CK_ATTRIBUTE_PTR>(kPrivKeyCreateTemplate),
                           _countof(kPrivKeyCreateTemplate), pubkey, prikey);

  if (rv != CKR_OK) return HandleEPassError(rv, "C_GenerateKeyPair");

  return Status();
}

static Status SaveDataObject(CK_SESSION_HANDLE session,
                             const std::string &label, void *data, size_t len,
                             CK_OBJECT_HANDLE *obj) {
  CK_ATTRIBUTE template1[] = {
      {CKA_CLASS, &kCKDataClass, sizeof(kCKDataClass)},
      {CKA_TOKEN, &kCKTrue, sizeof(kCKTrue)},
      {CKA_LABEL, void_ptr(label.c_str()), label.size()},
      {CKA_VALUE, data, len}, };
  CK_RV rv = ::C_CreateObject(session, template1, _countof(template1), obj);
  if (rv != CKR_OK) {
    return HandleEPassError(rv, "C_CreateObject");
  }
  return Status();
}

Status UKeyHelper::SaveSurDocCredential(uint32_t slot_id,
                                        const std::string &pin,
                                        const std::string &account,
                                        const ByteArray &credential) {
  CK_RV rv;
  CK_SESSION_HANDLE session = NULL_PTR;
  CK_OBJECT_HANDLE prikey = NULL_PTR;
  CK_OBJECT_HANDLE pubkey = NULL_PTR;
  CK_OBJECT_HANDLE acc_obj = NULL_PTR;
  CK_OBJECT_HANDLE priv_cipher_obj = NULL_PTR;
  CK_BYTE_PTR cred = static_cast<CK_BYTE_PTR>(void_ptr(&credential[0]));
  CK_ULONG enc_len = 0;
  CK_BYTE *enc_data = NULL_PTR;

  Status s = GlobalInit();
  if (!s.ok()) return s;

  s = OpenUserSession(slot_id, pin, false, &session);
  if (!s.ok()) {
    goto error;
  }

  s = SaveDataObject(session, SURDOC_ACCOUNT_LABEL, void_ptr(account.c_str()),
                     account.size(), &priv_cipher_obj);
  if (!s.ok()) {
    LOG_ERROR << "failed to save user account to token: " << s;
    goto error;
  }

  s = GenerateRsaKeyPair(session, &prikey, &pubkey);
  if (!s.ok()) {
    LOG_ERROR << "failed to generate RSA key pair in token: " << s;
    goto error;
  }

  rv = ::C_EncryptInit(session, const_cast<CK_MECHANISM_PTR>(&kEncMechanism),
                       pubkey);
  if (rv != CKR_OK) {
    s = HandleEPassError(rv, "C_EncryptInit");
    LOG_ERROR << "failed to encrypt user private key: " << s;
    goto error;
  }

  rv = ::C_Encrypt(session, cred, credential.size(), NULL_PTR, &enc_len);
  if (rv != CKR_OK) {
    s = HandleEPassError(rv, "C_Encrypt");
    LOG_ERROR << "failed to encrypt user private key: " << s;
    goto error;
  }

  enc_data = new CK_BYTE[enc_len];
  if (enc_data == NULL_PTR) {
    s = Status(Status::kOther, "Out of memory");
    goto error;
  }

  rv = ::C_Encrypt(session, cred, credential.size(), enc_data, &enc_len);

  if (rv != CKR_OK) {
    s = HandleEPassError(rv, "C_Encrypt");
    LOG_ERROR << "failed to encrypt user private key: " << s;
    goto error;
  }

  s = SaveDataObject(session, SURDOC_PRIV_KEY_CIPHER_LABEL, enc_data, enc_len,
                     &priv_cipher_obj);
  if (!s.ok()) {
    LOG_ERROR << "failed to save user private key cipher text to token: " << s;
    goto error;
  }

  ::C_CloseSession(session);
  return Status();

error:
  if (acc_obj != NULL_PTR) ::C_DestroyObject(session, acc_obj);
  if (prikey != NULL_PTR) ::C_DestroyObject(session, prikey);
  if (pubkey != NULL_PTR) ::C_DestroyObject(session, pubkey);
  if (priv_cipher_obj != NULL_PTR) ::C_DestroyObject(session, priv_cipher_obj);
  if (session != NULL_PTR) ::C_CloseSession(session);
  return s;
}

static bool s_epass_inited = false;
static dicom::Mutex s_epass_init_lock;
Status UKeyHelper::GlobalInit() {
  LockGuard g(s_epass_init_lock);
  if (s_epass_inited == true) return Status();

  CK_RV rv = ::C_Initialize(NULL_PTR);
  if (rv != CKR_OK) {
    LOG_ERROR << "PKCS#11::C_Initialize() failed: " << rv;
    return Status(Status::kOther, "PKCS#11::C_Initialize() failed");
  }
  s_epass_inited = true;
  return Status();
}

Status UKeyHelper::GlobalCleanup() {
  LockGuard g(s_epass_init_lock);
  if (s_epass_inited == false) return Status();

  CK_RV rv = ::C_Finalize(NULL_PTR);
  if (rv != CKR_OK) {
    LOG_ERROR << "PKCS#11::C_Finalize() failed: " << rv;
    return Status(Status::kOther, "PKCS#11::C_Finalize() failed");
  }
  s_epass_inited = false;
  return Status();
}
}
